La mayoría del contenido visto en Introducción a R se ha enfocado al uso del lenguaje R y al uso de ciertas funciones sin considerar su rendimiento. Como bien se menciono en su momento, R no esta diseñado para ser rápido; lo que a través de los años ha sido un tema de interés a medida que la información aumenta y las necesidades por analizar y ejecutar procesos que traten con grandes cantidades de datos. Por tales razones, el uso de paquetes que estén diseñados para trabajar de manera eficiente ha sido fundamental en la mayoría de lenguajes de programación; tal es el caso del Tidyverse.
Como bien se especifica en su página oficial, “The tidyverse is an opinionated collection of R packages designed for data science. All packages share an underlying design philosophy, grammar, and data structures”. Estos paquetes están enfocados a tener un mejor flujo de escritura, a tener un mejor entendimiento de la estructura de los procesos, dar funciones que solucionen problemas comunes y, como ya se menciono, mejorar el rendimiento de las funciones.
Dentro de este conjunto de paquetes se encuentran funciones para leer distintos tipos de archivos como aquellos con extensión .csv y .xls, además aquellas para reconocer archivos dados por SPSS, Stata y SAS, manipular archivos JSON, XML, dar una interfaz para trabajar con APIs, hacer web scraping y tener comunicación con diferentes administradores de bases de datos como SQL, MariaDB, etc.
Se tienen paquetes especializados en la manipulación y limpieza de datos, también para crear modelos con estos, dar características especiales como catalogar y tratar a ciertas variables como fechas y factores, aplicar técnicas de expresiones regulares en dichos datos, aplicar funciones optimizadas que pueden remplazar a las de la familia apply y crear gráficas profesionales con una mayor fluidez.
Aquí se verán solo algunos de los paquetes que componen todo este “universo limpio” y se comenzará con un operador fundamental proporcionado por el paquete magritt: %>%, el cual se podrá utilizar casi siempre en R.
Dicho operador tiene por nombre pipe, el cual tiene un uso similar al dado en otros lenguajes de programación como Python: . y bash: |. Este tiene como objetivo encadenar procesos de tal forma que el resultado dado en la cadena sirve como input del siguiente eslabón en dicha cadena, lo cual ayuda a evitar el anidamiento de funciones, minimizar la cantidad de objetos locales y facilitar la lectura e implementación de una secuencia de operaciones.
Supongase que se desea resolver la tarea sencilla de aplicar una cantidad definida de operaciones sobre un número; por ejemplo, obtener la raíz cuadrada del logaritmo natural de un número multiplicado por el cuadrado de otro, a dicho valor sumarlo con los primeros 10 naturales y, finalmente, obtener la raíz cuadrada de dicho resultado. Esto se puede resolver de la siguiente manera; supongamos que los números son 10 y 20.
[1] 7.607887
En tal caso se tuvo que anidar las funciones para aplicar estas cada resultado obtenido. Con %>% la solución se ve de esta forma
library(magrittr)
10 %>% # 10
prod(20^2) %>% #se multiplica por 20^2
log() %>% #se obtiene el logaritmo de dicho número
sqrt() %>% #Se obtiene la raíz cuadrada
sum(1:10) %>% #A eso se le suman los primeros 10 naturales
sqrt() #Se obtiene la raíz cuadrada.[1] 7.607887
La anterior solución tiene una estructura más fiel a como se fue resolviendo el problema poco a poco sin tiene que escribir hacia la izquierda para aplicar resultados de funciones anidadas. Más adelante se verá que el uso de dicho operador resulta fundamental para ahorra tiempo al momento de escribir código.
Otro ejemplo:
matrix(1:100, ncol = 5, byrow = 20) %>% #Creación de una matriz con los primeros 100 naturales.
rowSums(10) %>% #Se suma por renglón añadiendo 10 unidades a dicho resultado.
as.matrix() %>% #Se convierte dicho resultado a matriz.
scale() %>% #Se normalizan los datos.
sum() #Se comprueba que estos sumen media cero.[1] 0
Como la mayoría de la información que se utilizará se tendrá que cargar con alguna librería, aquí se dejan algunos ejemplos de algunas funciones útiles en la lectura de información.
La función read_csv() es una de las más comunes para leer archivos separados por comas, en la cual se puede dar el path del archivo o la dirección URL de dichos datos. En este caso se esta utilizando unos datos donde se relaciona la información sobre exceso de velocidad de ciertos automóviles y señales de advertencia. Para más información consúltese el siguiente enlace.
En caso de que se desee leer un archivo por algún otro delimitador, se puede usar la función read_delim() y en el caso de tener archivos donde el delimitador sea “\t” usar la función read_tsv(). Los datos que se utilizan son proporcionados por el US Census Bureau, los cuales fueron indirectamente obtenidos del siguiente enlance.
El paquete readxl esta diseño para leer archivos excel. La siguiente información proporciona la población por sexo y entidad federativa según grupos de edad quinquenales de acuerdo a los tabulados básicos del INEGI.
Algo que hay que tener en cuenta al trabajar con los paquetes del tidyverse es el uso de tibbles en lugar de data frames.
[1] "tbl_df" "tbl" "data.frame"
Un tibble es una versión moderna de un data frame que trabaja de manera perezosa (es decir, que realiza menos operaciones) evitando problemas comunes y supuestos que un data frame puede llegar a asumir. Por ejemplo, los tibbles no coercionan automáticamente los caracteres a factores, no crean nombres para las observaciones y no cambia los nombres de columnas que sean nombres no sintácticos, por ejemplo
Se pueden utilizar variables desde la construcción del tibble
data.frame(x = 1:20, y = x-1)?Además se tienen diferencias respecto a la impresión entre un tibble y un data frame, un tibble permite agregar listas directamente sin tener que usar la función I() y los tibbles nunca hacen emparejamiento parcial.
NULL
int [1:20] 1 2 3 4 5 6 7 8 9 10 ...
Finalmente, los tibbles permiten hacer substracciones con el operador pipe.
[1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
[1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
tribble()?En un análisis de datos, gran parte del tiempo que se utiliza es dando un grado de limpieza a estos, para así ya solo obtener información resumen, aplicar modelos o manipularlos para descubrir algo en ellos. De acuerdo a las fuentes que se consulten, esto puede tomar hasta un 80% del tiempo que se dispone, por lo que es evidente la necesidad de tener funciones que ayuden con dicha tarea.
La limpieza de datos varía dependiendo de los fines, pero para Hadley Wickham (Chief Scientist en RStudio y alguien importante en desarrollo y mantenimiento del tidyverse) tener una limpieza en los datos requiere de ciertas características. La información completa se puede encontrar en artículo de Hadley, Tidy Data, pero básicamente se tienen 3 puntos importantes para considerar que los datos son limpios.
Las siguientes gráficas representan dichos puntos sobre un subconjunto de Iris.
Con lo anterior establecido y suponiendo que se esta analizando una pequeña base de datos correspondientes a las calificaciones de ciertos alumnos ¿Cuál de las siguientes estructuras es correcta?
| Matemáticas | Química | |
|---|---|---|
| Juan | 8 | 9 |
| Carlos | 9 | 7 |
| Luis | 7 | 8 |
| Allison | 9 | 9 |
| Leticia | 8 | 9 |
| Juan | Carlos | Luis | Allison | Leticia | |
|---|---|---|---|---|---|
| Matemáticas | 8 | 9 | 7 | 9 | 8 |
| Química | 9 | 7 | 8 | 9 | 9 |
De hecho, ninguna de las dos opciones anteriores es correcta. De acuerdo a los principios anteriores para tener datos limpios, cada variable debe formar una columna, lo cual no sucede en este caso, ya que las variables aquí son estudiante o el nombre, la materia y las calificaciones. Es decir, que las configuración correcta es la siguiente
| Nombre | Asignatura | Calificaciones |
|---|---|---|
| Juan | Matemáticas | 8 |
| Carlos | Matemáticas | 9 |
| Luis | Matemáticas | 7 |
| Allison | Matemáticas | 9 |
| Leticia | Matemáticas | 8 |
| Juan | Química | 9 |
| Carlos | Química | 7 |
| Luis | Química | 8 |
| Allison | Química | 9 |
| Leticia | Química | 9 |
Esto es uno de los tantos ejemplos que se pueden dar cuando se trabajando con datos. En el momento en que se identifican estos problemas, se deberá usar toda la creatividad para resolverlos y obtener una estructura con la que ya se pueda trabajar. El paquete tidyr ayudará con la mayoría de estos.
Como recomendación, se aconseja tener a la mano siempre la respectiva Cheat Sheet que pueda ser de utilidad, en este caso de la librería tidyr se pueden consultar Data import y Data Wrangling; las respectivas traducciones al español se pueden encontrar en la página oficial de Cheat Sheets de RStudio.
Como bien dice Hadley en Tidy Data, hay cinco problemas comunes en los messy datasets:
En la sección anterior se presentaron los datos para la población por sexo y entidad federativa según grupos de edad quinquenales de acuerdo a los tabulados básicos del INEGI
colnames(Population_Sex_FEntity)[1:2] <- c("Entidad Federativa", "Población Total")
Population_Sex_FEntityEn las primeras dos columnas se cambiaron los nombres con las herramientas que hasta este momento se tienen. Véase que las demás columnas representan rangos de edad, por lo que se tiene el primer problema en los messy datasets. De hecho, en estos datos las variables son la Entidad Federativa, la Población Total, el Rango de Edad y el Porcentaje de la población total en ese rango de edad.
En estos casos se dice los datos son anchos, y lo que se desea es que estos estén en un formato largo (ancho por la cantidad de columnas y largo por la cantidad de observaciones). Para tales casos se utiliza la función tidyr::gather() la cual recibe como parámetros múltiples columnas y colapsa la información de estas en dos variables.
(Population_Sex_FEntity <- gather(data = Population_Sex_FEntity, key = "Rango_edad", value = "Porcentaje", -c("Entidad Federativa", "Población Total")))Con lo anterior se logro tener una mejor estructura en los datos, con lo cual fácilmente se podrían crear gráficas, modelos y estadísticas de resumen por Entidad Federativa o por rango de edad. Después de tener la información con una estructura adecuada, la imaginación es el único limitante. En el siguiente capítulo se verá como manipular internamente los datos para que se tenga una mejor semántica en ellos.
Si se desea por algún motivo, lo cual puede suceder, tener los datos en formato ancho, se puede usar la función complementaria la cual es tidyr::spread()
Finalmente:
tidyr:: gather() la proporciona el paquete reshape con su función melt().tidyr:: spread() se tiene la función reshape::dcast().gather() y spread() podrían haber sido remplazadas por las funciones tidyr::pivot_longer() y pivot_wider() respectivamente.Para ver un ejemplo del segundo punto, supóngase que los datos vistos al inicio donde se relacionaban las calificaciones de ciertos alumnos tienen originalmente la siguiente estructura
En tal caso, la segunda variable contiene la información tanto de la materia como de la calificación. Aquí la función tidyr::separate() es de gran ayuda
(Student_grades <- Student_grades %>% separate(col = "A/C", into = c("Asignatura", "Calificaciones"), sep = 1))sep se da la posición dentro del texto para separar los datos.En el caso en que se desee tener el caso inverso, se utiliza la función tidyr::unite()
Otro ejemplo lo proporciona la Cheat Sheet Data Wrangling.
tidyr::separate_rows()separate() esta dada por reshape::colsplit().En el caso que se tenga el problema donde algunas variables esten almacenadas tanto en renglones como columnas se debe tratar el problema como lo anterior visto, primero abstraerse al problema de juntar las columnas necesarias en dos variables con la función gather() para posteriormente separar las variables que lo requieran con la función separate().
Para los últimos dos casos se necesita hacer uso de unas funciones del paquete dplyr, el cual ser verá en la siguiente sección. Por el momento, se pueden ver otras funciones útiles de tidyr, como aquellas que ayudan en la obtención de diferentes operaciones de conjuntos en los datos.
La función tidyr::expand_grid() crea un tibble de todas las combinaciones de sus inputs, estos pueden ser data frames o tibbles, matrices y hasta vectores. Se puede entender a esta función como el producto cartesiano de dos conjuntos.
La función tidyr::crossing() regresa un tibble y tiene un comportamiento similar a expand_grid() con la diferencia de eliminar registros duplicados.
set.seed(20)
x <- data_frame(sample(letters[1:3], size = 5, replace = T))
y <- data_frame(y = sample(1:3, size = 5, replace = T))
tidyr::nesting(x, y)La función tidyr::nesting() encuentra las posibles combinaciones entre los datos, es decir, las combinaciones entre los datos de entrada si estos se unieran en uno solo.
set.seed(20)
reduced_iris <- iris %>% head()
df <- reduced_iris[sample(1:6, size = 10, replace = T),]
df %>% tidyr::expand(Petal.Length)La función tidyr::expand() genera todas las combinaciones de variables que se encuentran en un data set.
df %>% tidyr::expand(Sepal.Width, Petal.Width)?df %>% tidyr::expand(nesting(Sepal.Width, Petal.Width))?Otras funciones útiles son las siguientes
tidyr::nest(). Dicha función anida un data frame o un subconjunto de este en listas y coloca estos como observaciones; esto puede ser muy útil en la aplicación de modelos.tidyr::unnest(). Función inversa a tidyr::nest().tidyr::replace_na(). Dicha función encuentra los valores perdidos en un data set y los cambia por un valor dado.En la sección Limpieza de datos: Tidyr se vieron varios ejemplos donde se mencionaba que estos venían como sugerencias de las respectivas Cheat Sheets. Dichos datos fueron manipulados para ver el uso de las funciones que se estaban presentando. Obtén la estructura de los datos a los cuales se les aplicaron dichas funciones.
Investigar otras funciones de tidyr como chop(), complete() y pack().
Wickham, H. (2014). Tidy data. Journal of Statistical Software, 59(10), 1-23.
A work by Carlos Vásquez
cvasquezfguerra@gmail.com